﻿-- Author      : Thorwald Odin
-- Create Date : 12/7/2013 2:05:17 PM
local _, addon = ...; -- retrieve addon table

local L = addon.Locale;

local Recycler = LibStub('LibRecycler-1.0');

do -- Debug window, error handlers
	local timeZero = GetTime();
	local frame;
	local debugEvents = {};
	local maxDebugEvents = 100;
	local lastTime = 0;
	local timeAdjust = 0;

	local function Debug_UpdateData()
		local data = '';
		for i, v in ipairs(debugEvents) do
			if i <= maxDebugEvents and addon.db.DebugWatch[v.Type] then
				if addon.db.DebugWatch.Timestamp then
					data = data .. string.format("[%.3f seconds] ", v.Time);
				end
				data = data .. addon:Format("({1}) :: {2}\nStack trace:\n{3}\nLocal variables:\n{4}\n", v.Type, v.Message, v.Stack, v.Locals);
			end
		end
		if data == '' then
			data = L['There are no debug events on levels being watched.'];
		end
		frame.Data:SetText(data);
		frame.Data.Selected = false;
	end

	local function Debug_FrameShow()
		Debug_UpdateData();
	end

	local function Debug_FrameHide()
		frame.Data:SetText('');
	end

	local function Debug_CreateFrame()
		frame = getglobal("LootFilterDebugWindow");
		
		frame:SetScript("OnShow", Debug_FrameShow);
		frame:SetScript("OnHide", Debug_FrameHide);
		
		frame.Data:SetScript("OnTextChanged", Debug_UpdateData);
	end

	function addon:Error(msg, ...)
		self:Debug("error", msg, ...);
		geterrorhandler()(self:Format(msg, ...));
	end

	function addon:Assert(exp, msg, ...)
		if not exp then
			self:Error("Assertion failed!\n" .. tostring(exp) .. '\n' .. (msg or ""), ...);
		end
		return exp;
	end

	function addon:Debug(level, msg, ...)
		local msg = self:Format(msg, ...);
		local time = GetTime();
		if time == lastTime then -- We have a time conflict, so let's adjust the time a little.
			timeAdjust = timeAdjust + 1;
			time = time + (timeAdjust / 1000000); -- We use micro second adjustments to avoid multiple events having the same time stamp.
		else
			lastTime = time;
			timeAdjust = 0;
		end
		local stack = debugstack(0, 20, 20);
		local locals = debuglocals(0, 20, 20);
		local e = {
			Type = level,
			Message = msg,
			Stack = stack,
			Locals = locals,
			Time = time - timeZero, -- Calculate when the event occured, relative to the start of the session.
		};
		table.insert(debugEvents, e);
		return msg;
	end

	function addon:DisplayDebug()
		if not frame then
			Debug_CreateFrame();
		end
		table.sort(debugEvents, function(a, b)
			return a.Time > b.Time;
		end);
		while #debugEvents > maxDebugEvents do
			table.remove(debugEvents, maxDebugEvents + 1);
		end
		frame:Show();
	end
end

function addon:Format(msg, ...)
	local args = {...};
	
	local str = string.gsub(msg, "{(%d+)}", function(d)
		return tostring(args[tonumber(d)]);
	end);
	return str;
end

function addon:PrintF(msg, ...)
	local msg = self:Format(msg, ...);
	local t = {};
	for v in msg:gmatch("[^\n]+") do
		self:Print(v);
	end
end

do -- Call wrappers
	local function createMemberCallHandler(errH, object, func, args)
		local code = [[
			local eh, xpcall, object, ARGS = ...;
			local function call()
				return object:FUNC_NAME(ARGS);
			end
			local function Call()
				return xpcall(call, eh);
			end
			return Call;
		]];
		local ARGS = {'arg1'};
		for i = 2, #args do
			ARGS[i] = 'arg' .. i;
		end
		code = string.gsub(code, 'ARGS', table.concat(ARGS, ', '));
		code = string.gsub(code, 'FUNC_NAME', func);
		code = addon:Assert(loadstring(code, 'createMemberCallhandler(' .. func .. ')'));
		return code(errH, xpcall, object, unpack(args));
	end

	local function createCallHandler(errH, func, args)
		local code = [[
			local eh, xpcall, func, ARGS = ...;
			local function call()
				return func(ARGS);
			end
			local function Call()
				return xpcall(call, eh);
			end
			return Call;
		]];
		local ARGS = {'arg1'};
		for i = 2, #args do
			ARGS[i] = 'arg' .. i;
		end
		code = string.gsub(code, 'ARGS', table.concat(ARGS, ', '));
		code = addon:Assert(loadstring(code, 'createCallHandler'));
		return code(errH, xpcall, func, unpack(args));
	end

	addon.ErrorHandlers = {
		Default = function(...)
			return geterrorhandler()(addon:Debug('error', ...));
		end,
		Silent = function(...)
			return addon:Debug("error", ...);
		end,
		Warning = function(...)
			return addon:Debug("warning", ...);
		end,
	};

	function addon:CreateCallHandler(data)
		local errH = data.ErrorHandler or self.ErrorHandlers.Default;
		local object = data.Object or nil;
		local func = data.Function or self:Error('CreateCallHandler: data.Function is nil');
		local args = data.Args or {};
		if type(errH) == 'string' then
			errH = self.ErrorHandlers[errH] or self.ErrorHandlers.Default;
		end
		if type(func) == 'string' and not object then
			object = addon;
		end
		if object then
			return createMemberCallHandler(errH, object, func, args);
		else
			return createCallHandler(errH, func, args);
		end
	end
end

do -- Table methods
	local function tableDump(arr, depth, maxDepth, pairFunc)
		if maxDepth >= 0 and depth > maxDepth then
			return tostring(arr);
		end
		local text, value, padding, item = "", nil, "", nil;
		for i = 1, depth do
			padding = padding .. "    ";
		end
		local data = "";
		if type(arr) == "table" then
			for item, value in pairFunc(arr) do
				if type(item) == 'string' then
					item = ('"%s"'):format(item);
				else
					item = tostring(item);
				end
				if type(value) == 'table' then
					data = addon:Format("{\n{1}{2}}", tableDump(value, depth + 1, maxDepth, pairFunc), padding);
				elseif type(value) == 'string' then
					data = addon:Format('"{1}"', value);
				else
					data = tostring(value);
				end
				text = addon:Format("{1}{2}[{3}] = {4},\n", text, padding, item, data);
			end
		end
		return text;
	end

	function addon:TableDumpKVPairs(tbl)
		return tableDump(tbl, 0, -1, pairs);
	end

	function addon:TableDumpIVPairs(tbl)
		return tableDump(tbl, 0, 1, ipairs);
	end
	
	function addon:TableCopy_Deep(tbl)
		local t = {};
		for k, v in pairs(tbl) do
			if type(v) == 'table' then
				t[k] = addon:TableCopy_Deep(v);
			else
				t[k] = v;
			end
		end
		return t;
	end
	
	function addon:TableCopy_Shallow(tbl)
		local t = {};
		for k, v in pairs(tbl) do
			t[k] = v;
		end
		return t;
	end
end

do -- Object recycler methods
	local function NewTable()
		return {};
	end
	
	addon.tableRecycler = Recycler:NewRecycler('table', NewTable, wipe);
end

do -- Event scheduler
	local SchedulerWorker, ScheduleFrame, Scheduler, Scheduler_OnUpdate;

	ScheduleFrame = CreateFrame("Frame", "LootFilterScheduleFrame", UIParent);

	Scheduler_OnUpdate = function()
		if not Scheduler then
			Scheduler = coroutine.wrap(SchedulerWorker)
			Scheduler(addon);
		end
		Scheduler();
	end
	
	SchedulerWorker = function(addon)
		local self = addon;
		local StartTime, MaxTime = 0, 0.005; -- 5 millisecond maximum. @TODO: option to configure max time
		local function Yield(force)
			if force or StartTime + MaxTime > GetTime() then
				coroutine.yield();
			end
		end
		local stack = self.ScheduleStack;
		local event = nil;
		local force = false; -- force Yield to call coroutine.yield
		local Time, Func;
		Yield(true); -- Done setting up
		while true do
			StartTime = GetTime();
			force = false;
			event = stack[1];
			if event then
				Time, Func = event.Time, event.Function;
				if Time < StartTime then
					Func();
					table.remove(stack, 1);
				end
			else
				force = true;
			end
			Yield(force);
		end
	end

	function addon:SetupScheduler()
		self.ScheduleStack = {};
		Scheduler = coroutine.wrap(SchedulerWorker);
		Scheduler(addon); -- Initialize Scheduler coroutine.
		ScheduleFrame:SetScript("OnUpdate", Scheduler_OnUpdate);
	end

	function addon:Schedule(delay, data) -- delay is in miliseconds
		local func = self:CreateCallHandler(data);
		local event = {
			Time = GetTime() + delay,
			Function = func,
		}
		table.insert(self.ScheduleStack, event);
		table.sort(self.ScheduleStack, function(a, b)
			return a.Time < b.Time;
		end);
	end
end

